Skip to content

attributes#125

Merged
wol-soft merged 31 commits intomasterfrom
attributes
Apr 17, 2026
Merged

attributes#125
wol-soft merged 31 commits intomasterfrom
attributes

Conversation

@wol-soft
Copy link
Copy Markdown
Owner

@wol-soft wol-soft commented Apr 3, 2026

No description provided.

Enno Woortmann and others added 14 commits March 26, 2026 01:35
Add DraftInterface/DraftFactoryInterface abstraction and wire it into
GeneratorConfiguration. The pipeline does not yet use the draft — this
phase establishes the structural foundation only.

New classes:
- DraftInterface: getDefinition(): DraftBuilder
- DraftFactoryInterface: getDraftForSchema(JsonSchema): DraftInterface
- DraftBuilder: fluent builder for Draft instances
- Draft: built result with getCoveredTypes(string|array) — always includes 'any'
- Element/Type: holds a type name and its ModifierInterface list
- Modifier/ModifierInterface: modify() contract for future phases
- Draft_07: registers all 7 JSON Schema types + 'any' (empty modifier lists)
- AutoDetectionDraft: DraftFactoryInterface that detects $schema keyword,
  falls back to Draft_07 for absent/unrecognised values

GeneratorConfiguration gains getDraft()/setDraft() accepting
DraftInterface|DraftFactoryInterface; defaults to AutoDetectionDraft.

phpcs.xml: exclude Squiz.Classes.ValidClassName.NotPascalCase to allow
underscore-separated draft class names (Draft_07, Draft_2020_12, etc.).
…raft architecture

Phase 2 — Eliminate PropertyMetaDataCollection:
- Add bool $required parameter to PropertyFactory::create,
  ProcessorFactoryInterface::getProcessor and all processor constructors
- Replace isAttributeRequired() lookups with $this->required throughout
- Move addDependencyValidator to BaseProcessor::addPropertiesToSchema,
  reading directly from $json['dependencies'][$propertyName]
- Update SchemaDefinition::resolveReference to accept bool $required
  instead of PropertyMetaDataCollection; simplify cache key accordingly
- Remove PMC save/restore from AdditionalPropertiesValidator,
  PatternPropertiesValidator, ArrayTupleValidator,
  AbstractComposedValueProcessor, and IfProcessor
- Delete PropertyMetaDataCollection entirely

Phase 3 refinements — Draft architecture:
- DraftInterface.getDefinition() returns DraftBuilder (not Draft),
  enabling draft extension by consumers; PropertyFactory builds and caches
- Type constructor auto-installs TypeCheckModifier via
  TypeConverter::jsonSchemaToPhp, removing per-type boilerplate from Draft_07
- DefaultValueModifier is self-contained: reads type from schema, resolves
  is_* check and int->float coercion internally; no callable constructor API
- Draft_07 is now a clean declarative list with no implementation detail
- Replace DRAFT_BYPASS_TYPES hardcoded list with Draft::hasType() check
- Add TypeConverter::jsonSchemaToPhp() to centralise JSON->PHP type mapping
…to validator factories

Introduce AbstractValidatorFactory hierarchy (SimplePropertyValidatorFactory,
SimpleBaseValidatorFactory) and migrate all keyword-specific validator generation
out of the legacy processors into dedicated factory classes registered on Draft_07.

Migrated types: string (pattern, minLength, maxLength, format), integer/number
(minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf), array
(items, minItems, maxItems, uniqueItems, contains), universal (enum, filter).
Also adds DefaultArrayToEmptyArrayModifier for the array default-empty behaviour.

Legacy processor generateValidators overrides for string, numeric, and array
types are removed entirely. AbstractNumericProcessor is now empty (flattened into
IntegerProcessor/NumberProcessor directly extending AbstractTypedValueProcessor).

PropertyFactory exposes applyTypeModifiers/applyUniversalModifiers as public
methods; MultiTypeProcessor uses these directly instead of a skipUniversalModifiers
flag, eliminating the 'type-only' sentinel.

FilterValidator.validateFilterCompatibilityWithBaseType now derives the effective
type from getNestedSchema() for object properties instead of relying on a
set/restore workaround in FilterValidatorFactory.
Multi-type properties ("type": [...]) are now handled directly in
PropertyFactory::createMultiTypeProperty instead of delegating to
MultiTypeProcessor. Each type is processed through its legacy single-type
processor and Draft type modifiers; type-check validators are collected and
consolidated into a single MultiTypeCheckValidator; decorators are forwarded
via PropertyTransferDecorator; universal modifiers run once on the main
property after all sub-properties resolve.

- Delete MultiTypeProcessor
- PropertyProcessorFactory::getProcessor now only handles string types;
  the private getSingleTypePropertyProcessor wrapper is inlined
- ProcessorFactoryInterface::getProcessor parameter narrowed to string
- Type validity is checked via PropertyFactory::checkType (shared between
  scalar and multi-type paths)
- Three invalidRecursiveMultiType test expectations updated: outer
  InvalidItemException property name changes from "item of array property"
  to "property" due to different extracted method registration order
…r architecture

- Convert MinProperties, MaxProperties, PropertyNames, PatternProperties,
  AdditionalProperties, and Properties processing from BaseProcessor methods
  into validator factories (Factory/Object/) registered via addValidator() in
  Draft_07, consistent with the scalar/array/number/string factory pattern
- ObjectModifier remains a proper modifier (structural, not keyword-driven)
- PropertyFactory: add type=object path that calls processSchema directly,
  wires the outer property via TypeCheckModifier + ObjectModifier, and runs
  universal modifiers (filter/enum/default) on the outer property
- PropertyFactory: add RequiredPropertyValidator for required type=object
  properties; strip property-level keywords before passing schema to
  processSchema to prevent double-application on the nested class root
- SchemaProcessor: add transferComposedPropertiesToSchema (migrated from
  BaseProcessor) with correct use imports so allOf/anyOf/oneOf branch
  properties are transferred and conflict detection fires correctly
- BaseProcessor: remove all methods now handled by Draft modifiers/factories
…dValueProcessorFactory

Introduce Model\Validator\Factory\Composition\AbstractCompositionValidatorFactory
(extends AbstractValidatorFactory) with shared composition helpers, plus five
concrete factories: AllOfValidatorFactory, AnyOfValidatorFactory,
OneOfValidatorFactory, NotValidatorFactory, IfValidatorFactory. A marker
interface ComposedPropertiesValidatorFactoryInterface replaces
ComposedPropertiesInterface for the property-transfer guard.

Register all five on the 'any' type in Draft_07 via addValidator(), consistent
with the keyword-keyed pattern established in Phase 4.

Delete ComposedValueProcessorFactory and all legacy ComposedValue processor
classes (AbstractComposedValueProcessor, AllOfProcessor, AnyOfProcessor,
OneOfProcessor, NotProcessor, IfProcessor and their interfaces).
AbstractPropertyProcessor is slimmed to only the RequiredPropertyValidator and
isImplicitNullAllowed helpers still needed by the bridge.

Update all is_a() checks and use-imports in Schema, BaseProcessor,
SchemaProcessor, ConditionalPropertyValidator, and
CompositionRequiredPromotionPostProcessor to reference the new factory classes.
…ConstModifier, IntToFloatModifier, NullModifier

- Delete all PropertyProcessor/Property/* classes (AbstractPropertyProcessor,
  AbstractValueProcessor, AbstractTypedValueProcessor, AbstractNumericProcessor,
  StringProcessor, IntegerProcessor, NumberProcessor, BooleanProcessor,
  ArrayProcessor, ObjectProcessor, NullProcessor, AnyProcessor, ConstProcessor,
  ReferenceProcessor, BasereferenceProcessor, BaseProcessor)
- Delete PropertyProcessorFactory and ProcessorFactoryInterface
- Remove ProcessorFactoryInterface constructor parameter from PropertyFactory
- Inline $ref / baseReference routing as private methods on PropertyFactory
- Refactor PropertyFactory::create into focused dispatch + four extracted methods
  (createObjectProperty, createBaseProperty, createTypedProperty, buildProperty)
  with a single unified applyModifiers helper replacing three separate methods
- Add ConstModifier (registered on 'any' type): sets PropertyType from const value
  type and adds InvalidConstException validator; immutability is fully respected
- Add IntToFloatModifier (registered on 'number' type): adds IntToFloatCastDecorator
- Add NullModifier (registered on 'null' type): clears PropertyType and adds TypeHintDecorator
- Register object type in Draft_07 with typeCheck=false (PropertyFactory handles
  object type-check via wireObjectProperty, not via Draft modifier dispatch)
- Remove stale PropertyProcessorFactory imports from validator/schema files
- Add ConstPropertyTest::testConstPropertyHasNoSetterWhenImmutable
- Add docs/source/generator/custom/customDraft.rst: custom draft / modifier guide
- Update setDraft documentation in gettingStarted.rst with seealso link
- Update CLAUDE.md architecture section to document the final Draft modifier system
@coveralls
Copy link
Copy Markdown

coveralls commented Apr 3, 2026

Coverage Report for CI Build 24573811184

Coverage decreased (-0.1%) to 98.551%

Details

  • Coverage decreased (-0.1%) from the base build.
  • Patch coverage: 22 uncovered changes across 8 files (1815 of 1837 lines covered, 98.8%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
src/SchemaProcessor/PostProcessor/Internal/TransformingFilterOutputTypePostProcessor.php 55 44 80.0%
src/Utils/FilterReflection.php 77 73 94.81%
src/SchemaProcessor/SchemaProcessor.php 109 107 98.17%
src/Model/Property/PropertyProxy.php 1 0 0.0%
src/Model/SchemaDefinition/JsonSchema.php 19 18 94.74%
src/Model/Validator/Factory/Composition/AbstractCompositionValidatorFactory.php 90 89 98.89%
src/Model/Validator/PropertyNamesValidator.php 14 13 92.86%
src/PropertyProcessor/PropertyFactory.php 292 291 99.66%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
src/Model/Property/PropertyProxy.php 1 60.32%

Coverage Stats

Coverage Status
Relevant Lines: 4624
Covered Lines: 4557
Line Coverage: 98.55%
Coverage Strength: 610.41 hits per line

💛 - Coveralls

Enno Woortmann and others added 15 commits April 3, 2026 03:24
…t keys (#103)

- SerializationPostProcessor: key skipNotProvidedPropertiesMap by schema name
  (getName()) rather than the PHP attribute name (getAttribute(true)) so the
  array_diff comparison against rawModelDataInput works correctly
- SerializableTrait: use #[SchemaName] reflection to map PHP property names to
  their original JSON Schema names; fix getCustomSerializerMethod cache to be
  per-class (static::class key) and use array_key_exists instead of isset to
  correctly handle the false sentinel; remove str_starts_with('_') fallback —
  #[Internal] is now the sole exclusion mechanism
- Add Internal attribute to production library; mark all three static trait
  properties with #[Internal]
- Update evaluateAttribute: use jsonSerialize/toArray with correct depth
  propagation; cache serialization capability per class (not per call); handle
  depth-exhausted path after capability cache to avoid poisoning
- $except now takes schema names only (breaking change)
- Tests: add Issue103Test (4 combined tests), extend PhpAttributeTest with
  leading-digit schema name case, fix stale testBuiltinAttributes assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerated internal properties

Previously, properties generated as internal (additional properties, pattern
properties, rawModelDataInput, errorRegistry, etc.) used a leading underscore
as both a naming convention and a serialization exclusion signal. This was an
implicit contract that polluted the namespace and conflicted with clean PHP
naming.

Changes:
- AbstractProperty::getAttribute(): no longer prepends '_' for internal properties
- Property::setInternal(true): auto-adds #[Internal] PHP attribute to the
  property; the flag is one-way (calling setInternal(false) after true is a
  no-op since the attribute cannot be removed from the list)
- RenderJob::getUseForSchema(): always includes Internal::class in the use list
  so the hardcoded rawModelDataInput/errorRegistry declarations in Model.phptpl
  can carry the #[Internal] annotation
- Model.phptpl: rawModelDataInput and errorRegistry declared with #[Internal];
  all $this->_* references updated throughout
- BuilderClass.phptpl: rawModelDataInput and errorRegistry renamed
- All validator and post-processor templates updated (errorRegistry,
  additionalProperties, patternProperties, patternPropertiesMap,
  propertyValidationState, skipNotProvidedPropertiesMap, rawModelDataInput)
- PHP source files updated (RenderHelper, validator factories,
  post-processors) where property names were emitted as strings
- SerializableTrait fallback path: removed str_starts_with('_') check —
  #[Internal] is now the sole mechanism for excluding properties in the
  fallback (non-generated) serialization path
- Test: fix Issue116Test to reference 'additionalProperties' (not
  '_additionalProperties') when reflecting on generated class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This reverts commit ef5550c.
…nd filter tests

- Type::addValidator now stores validators under their key in the modifiers array,
  so a second addValidator call with the same key replaces the existing entry
- Add DraftExtensibilityTest covering custom validator override, chained validators,
  and new type registration
- Add PassThroughTypeCheckValidatorTest
- Add object-level composition (allOf/not) schema fixtures and test coverage
- Add PropertyNamesValidator and PropertyNamesTest coverage
- Add test confirming filters with empty acceptedTypes are compatible with any property type
- CLAUDE.md: add clarification policy, test consolidation rules, and other project guidance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erValidator

- Rename TypeConverter::jsonSchemaToPhp to jsonSchemaToPHP and update all call sites
- Replace mapDataTypes wrapper in FilterValidator with direct array_map(TypeConverter::jsonSchemaToPHP(...), ...)
- Use constructor property promotion for $transformingFilter
- Refactor validateFilterCompatibilityWithBaseType into runCompatibilityCheck, which
  handles both typed properties (existing logic) and untyped properties (new: throws
  SchemaException when the filter does not cover every PHP primitive type)
- Update constructor guard: typed/nested/transforming-filter cases check immediately;
  untyped + non-transforming filter defers to the upcoming FilterCompatibilityPostProcessor
- Add public validateCompatibilityWithProperty for the post-processor to call
- Guard typeCheck template value against 'mixed' in acceptedTypes (no PHP runtime check)
- Patch validateFilterCompatibilityWithTransformedType to handle null/mixed return types
  from transforming filters (unconstrained output requires the subsequent filter to
  accept all types)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check threw SchemaException on any partial type mismatch.
The new rule: only throw when a typed property has zero overlap with the
filter's acceptedTypes (the filter can never execute). Partial overlap is
valid — the runtime typeCheck guard in the generated code already skips
the filter for non-matching value types.

- Untyped properties are always OK: any non-empty acceptedTypes has
  overlap with the infinite type space
- filter accepts all types ([] or 'mixed'): always OK
- Typed property, zero overlap: SchemaException
- Typed property, partial or full overlap: OK

Simplifications enabled by this design:
- No post-processor needed (FilterCompatibilityPostProcessor dropped)
- No deferred check path ($compatibilityChecked flag removed)
- No validateCompatibilityWithProperty public method
- runCompatibilityCheck reduced to an intersection check

Update existing tests to reflect the new behaviour:
- testFilterWhichAppliesToMultiTypePropertyPartiallyThrowsAnException
  renamed and converted to a success-path test (partial overlap is now OK)
- testRestrictedFilterOnUntypedPropertyIsAllowed documents that untyped
  properties with restricted filters are valid

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mbination

Add 'mixed' to the valid acceptedTypes values in GeneratorConfiguration::addFilter.
Also enforce that 'mixed' must not be combined with other types, since that would
be contradictory — throw InvalidFilterException if the combination is detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Filter.phptpl template now uses && short-circuit so the filter
callable is only invoked when the runtime value type is in the filter's
acceptedTypes list. Non-matching types silently skip the filter.

The typeCheck expression is changed from negative (throw path) to
positive (is_type($value) || ...) joined with ||, so the && condition
passes only for matching types.

Tests updated: renamed/added cases to assert generation succeeds for
partial-overlap and untyped-property scenarios, and to verify runtime
filter-skip behaviour for non-matching types.

Also fixes stray `new \DateTimeZone()` line in TypeConverter and the
`end(explode(...))` PHP notice in FilterValidator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rst parameter

FilterInterface::getAcceptedTypes() is removed (see production repo commit).
Accepted types are now derived automatically from the first parameter's
type hint of the filter callable via FilterValidator::getAcceptedTypes()
(private, non-static).

- No type hint or mixed parameter → [] (accepts all, no type guard)
- ReflectionNamedType → [name] plus 'null' if allowsNull()
- ReflectionUnionType → all constituent type names

The mixed+other-types guard is dropped: PHP's type system makes
mixed|OtherType a parse error so the check was unreachable via reflection.

FilterProcessor restructured to a single instanceof check per iteration:
validation, type wiring, addValidator, and pass-through all happen in one
block; addTransformedValuePassThrough runs after addValidator so the
transforming filter's own FilterValidator also receives the check.

Tests updated: helpers drop acceptedTypes parameter; callables carry their
own type hints; the removed invalid-type-name test is deleted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace FilterInterface::getAcceptedTypes() with reflection on the
callable's first parameter type hint (FilterReflection utility). Filters
with no type hint now throw SchemaException at generation time; mixed
accepts all types with no runtime guard. Extract TypeCheck utility for
building is_type/instanceof expressions used in generated code.

Introduce TransformingFilterOutputTypePostProcessor (always-on internal
post-processor) that computes the output type for properties with a
transforming filter after composition has fully resolved the base type.
FilterProcessor eagerly applies the output type when the base type is
already known at filter-processing time; the post-processor handles the
deferred case where the type comes from a sibling allOf branch.

PassThroughTypeCheckValidator updated to accept string[] instead of a
ReflectionType and now deduplicates its type list. Dead $isBuiltin
parameter and unused fromReflectionType factory removed from
ReflectionTypeCheckValidator.

CLAUDE.md: add rule that filter callables may only reference
production-library classes (generator-package callables would be embedded
in generated code and fail at runtime without the generator installed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Phase 4d tests for output type formula, reflection-based accepted types,
  and filter chain behaviour (R2, R6, R7, F3-F7, F4, CH2)
- Change FilterReflection to throw InvalidFilterException (not SchemaException)
  for missing/void/never return types and missing parameter type hints, since
  the error is in the filter definition rather than the schema; add tests F5-F7
- Add SingleFileProvider with tests covering happy path, invalid input,
  getBaseDirectory(), and cross-file $ref resolution
- Fix SingleFileProvider constructor to avoid TypeError when json_decode returns
  null on invalid JSON; suppress file_get_contents warning for missing files
- Replace all inline JSON schemas in FilterTest with dedicated schema files
  under tests/Schema/FilterTest/ to follow the established test suite pattern
- Update filter.rst: rewrite overlap/skip behaviour, remove IncompatibleFilterException
  section, update custom filter examples to use type hints instead of getAcceptedTypes()

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@wol-soft wol-soft merged commit 0fa6e72 into master Apr 17, 2026
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants